﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using GoodStuff.Net;

namespace GoodStuff.Diagnostics
{
    public class ExceptionHandlerModule : IHttpModule
    {
        private static IList<IExceptionHandler> __handlers = new List<IExceptionHandler>();

        public void Init(HttpApplication context)
        {
            ExceptionHandlerConfiguration configuration = ExceptionHandlerConfigurationSectionHandler.GetConfiguration("goodStuff/exceptionHandlerConfiguration");
            if (configuration != null && configuration.Enabled)
            {
                lock (__handlers)
                {
                    __handlers.Clear();
                    if (configuration.Mailer != null)
                    {
                        __handlers.Add(new ExceptionHandlerMailer(configuration.Mailer));
                    }
                    if (configuration.Sql != null)
                    {
                        __handlers.Add(new ExceptionHandlerSql(configuration.Sql));
                    }
                }

                context.Error += new EventHandler(context_Error);
            }
        }      

        /// <summary>
        /// To add a custom handler, register your implementation once in global.asax or in a HttpModule/Handler.
        /// </summary>
        /// <param name="handler"></param>
        public static void AddExceptionHandler(IExceptionHandler handler)
        {
            lock (__handlers)
            {
                __handlers.Add(handler);
            }
        }

        public void Dispose()
        {            
        }

        private void context_Error(object sender, EventArgs e)
        {
            HttpApplication app = (HttpApplication)sender;
            Exception error = app.Server.GetLastError();

            // http://msdn.microsoft.com/en-us/library/system.exception.getbaseexception.aspx
            // "Use the GetBaseException method when you want to find the root cause of an exception but do not need information about 
            //  exceptions that may have occurred between the current exception and the first exception."
            if (error is HttpException && error.InnerException != null)
            {
                //error = error.GetBaseException();     --> baseException naar Log implementatie.
                error = error.InnerException;
            }

            LogError(error);
         

            //ONLY clear the error when we provide custom output.
            //app.Server.ClearError();
        }

        /// <summary>
        /// External entry point so you can log your own exception with the handler.
        /// </summary>
        /// <param name="error"></param>
        public static void LogError(Exception error)
        {
            lock (__handlers)
            {
                foreach (var handler in __handlers)
                {
                    try
                    {
                        HttpRequest request = HttpContext.Current.Request;

                        //exclusion gaat op de BaseException
                        if (MatchesExcludeRules(error.GetBaseException(), handler.Excludes, request) == false)
                        {
                            handler.HandleException(error, request);
                        }
                    }
                    catch (Exception exception)
                    {
                        System.Diagnostics.Trace.WriteLine("Error while handling exception: " + exception.ToString());
                    }
                }
            }
        }

        /// <summary>
        /// Takes the error and check the excluderules from the configuration and returns a boolean indicating when at least one rule has been matched
        /// </summary>
        /// <param name="error">The error that is thrown</param>
        /// <param name="excludes">The excluderules</param>
        /// <param name="request">The HttpRequest-object of the current request</param>
        /// <returns>A booleand indicating at least one excluderule has been matched</returns>
        protected static bool MatchesExcludeRules(Exception error, IList<ExceptionHandlerExclude> excludes, HttpRequest request)
        {
            //If no error or exluderules are available, we can't match at all.
            if (error == null || excludes == null || excludes.Count == 0)
            {
                return false;
            }

            //First take a look for sourceIP's that we want to exclude
            if (request != null && !string.IsNullOrEmpty(request.UserHostAddress))
            {
                //Take all exclusions and grab the sourceIP of each exlusion ... from exclusions where a sourceIP has been configured.
                string[] sourceIPsToExclude = excludes.Where(e => e.SourceIP != null && e.SourceIP != string.Empty).Select(e => e.SourceIP).ToArray();
                if (IPUtility.MatchAny(request.UserHostAddress, sourceIPsToExclude))
                {
                    //SourceIP matches at least one of the excluderules. Errors from this source should be ignored
                    return true;
                }
            }

            //Secondly we have to check the errortype and/or message

            //Multiple <Exclude> can be configured. If in one <Exclude> both an exclude for type AND message are configured, we have to perform
            //an "AND". If only a type or a message is configured in a single <Exclude> we are performing an "OR".

            foreach (ExceptionHandlerExclude excludeRule in excludes.Where(e => e.Message != null || e.TypeName != null))
            {
                //We have an exclude with a type or a message to filter on.
                bool performAndCompare = !string.IsNullOrEmpty(excludeRule.TypeName) && !string.IsNullOrEmpty(excludeRule.Message);
                bool matchOnType = false;
                bool matchOnMessage = false;


                if (!string.IsNullOrEmpty(excludeRule.TypeName) && MatchType(error.GetType(), excludeRule.TypeName))
                {
                    matchOnType = true;
                }

                //Check if we have a match with the message of the main-exception
                if (!string.IsNullOrEmpty(excludeRule.Message) && MatchMessage(error.Message, excludeRule.Message, excludeRule.CompareMethod))
                {
                    matchOnMessage = true;
                }

                //can't return directly with a OR, we only have to return when matchOnType or matchOnMessage is true.
                //Otherwise we have to continue to the next rule.
                if (performAndCompare && matchOnType && matchOnMessage) //AND
                {
                    return true;
                }
                else if (performAndCompare == false && (matchOnType || matchOnMessage)) //OR
                {
                    return true;
                }
            }

            //no matches at this point
            return false;
        }

        /// <summary>
        /// Matches the sourceType against the given typename. Return true if a match is found.
        /// </summary>
        /// <param name="sourceType"></param>
        /// <param name="compareToTypeName"></param>
        /// <returns></returns>
        private static bool MatchType(Type sourceType, string compareToTypeName)
        {
            //Breakout when no input is available
            if (sourceType == null || string.IsNullOrEmpty(compareToTypeName))
            {
                return false;
            }

            Type t = Type.GetType(compareToTypeName, false);
            if (t != null && t == sourceType)
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Matches the sourceMessage against the compareToMessage with the given ExceptionHandlerExceptionMessageCompareMethod
        /// </summary>
        /// <param name="sourceMessage"></param>
        /// <param name="compareToMessage"></param>
        /// <param name="compareType"></param>
        /// <returns></returns>
        private static bool MatchMessage(string sourceMessage, string compareToMessage, ExceptionHandlerExceptionMessageCompareMethod compareType)
        {
            switch (compareType)
            {
                case ExceptionHandlerExceptionMessageCompareMethod.Exact:
                    if (string.Equals(sourceMessage, compareToMessage))
                    {
                        return true;
                    }
                    break;
                case ExceptionHandlerExceptionMessageCompareMethod.Contains:
                    if (sourceMessage.Contains(compareToMessage))
                    {
                        return true;
                    }
                    break;
                case ExceptionHandlerExceptionMessageCompareMethod.StartsWith:
                    if (sourceMessage.StartsWith(compareToMessage))
                    {
                        return true;
                    }
                    break;
                default:
                    {
                        //We are doing this while handling an error, so we don't want to throw an error again
                        System.Diagnostics.Trace.Write(compareType.ToString() + " is an unkown ExceptionHandlerExceptionMessageCompareMethod.", "GoodStuff.Diagnostics");
                        break;
                    }
            }

            return false;
        }
    }
}
